跳到主要内容

SpringCloud Hystrix 服务降级

什么是服务降级

所谓降级,就是整体资源快不够用了,忍痛将某些服务先关掉,待度过难关,在开启回来。一般是从整体符合考虑,当某个服务熔断之后,服务器将不再被调用,此刻客户端可以自己准备一个本地的 fallback 回调,返回一个缺省值,这样做,虽然服务水平下降,但好歹可用,比直接挂掉要强。

配置环境

<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-netflix-hystrix -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

<!-- Hystrix-Dashboard -->
<!-- 注意这个不是必须的,它就是一个监控工具 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>

基本配置降级使用

在启动类里加上这个 @EnableHystrix 注解

就是当要调用的服务挂了之后,默认返回一个托底数据防止当前服务也无法运行

@SpringBootApplication
@EnableFeignClients //开启Feign
@EnableEurekaClient
@EnableHystrix
public class EurekaClientApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaClientApplication.class, args);
}
}

然后就可以直接在这个方法上使用 @HystrixCommand 注解加上 fallback 的目标方法(其实这个 @HystrixCommand 可以写在 Service 层)

@GetMapping("/search/{id}")
@HystrixCommand(fallbackMethod = "findByIdFallBack")
public Customer findById(@PathVariable Integer id) {
return searchClient.findById(id);
}

// findById 的降级方法,方法描述要和接口一致
public Customer findByIdFallBack(@PathVariable Integer id) {
return new Customer(-1, "", 0);
}

这里调用的 search 服务可以内部搞一个耗时操作,然后使用 JMeter 进行压力测试试试

private static final Object lock = new Object();
private static int count = 10;

public String payment(Integer id) {
int timeNumber = 500000;
// 可以使用 synchronized 来模拟这个耗时操作
for (int i = 0; i < timeNumber; i++) {
synchronized (lock) {
count++;
}
}

return id;
}

然后尝试一下两万的并发量

在 Service 层配置服务降级

除了像上面那样直接在 Controller 层配置服务降级,还可以在 Service 层(生产者服务)配置服务降级

生产者服务的 API

@Resource
private PaymentService paymentService;

@GetMapping(value = "/payment/hystrix/timeout/{id}")
public String paymentInfoTimeOut(@PathVariable("id") Integer id) {
String result = paymentService.paymentInfoTimeout(id);
log.info("result:" + result);
return result;
}

调用的 Service 层

/**
* 这个 @HystrixCommand 报异常后如何处理: 一旦调用服务方法失败并抛出了错误信息后,
* 会自动调用 @HystrixCommand 标注好的 fallbackMethod 调用类中的指定方法
*/
@HystrixCommand(
fallbackMethod = "paymentInfoTimeOutHandler",
commandProperties = {
// 设置这个线程的超时时间是3s,3s内是正常的业务逻辑,超过3s调用 fallbackMethod 指定的方法进行处理
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
})
public String paymentInfoTimeout(Integer id) {
int timeNumber = 5;
try {
TimeUnit.SECONDS.sleep(timeNumber); // 模拟超时错误
} catch (InterruptedException e) {
e.printStackTrace();
}
return
"当前调用的方法是:" + "paymentInfoTimeout" + "\n" +
"当前线程池:" + Thread.currentThread().getName() + "\n" +
"当前传入的 ID:" + id + "\n" +
"总耗时(秒):" + timeNumber;
}



public String paymentInfoTimeOutHandler(Integer id) {
return
"当前调用的方法是:" + "paymentInfoTimeout" + "\n" +
"当前线程池:" + Thread.currentThread().getName() + "\n" +
"当前传入的 ID:" + id + "\n" +
"系统繁忙,请稍后再试";
}

在 Feign 服务配置服务降级

除了在 Service 层配置服务降级,还可以在消费者端的 Feign 服务里配置

编写的 Feign 服务

@Component
@FeignClient(
value = "PROVIDER-HYSTRIX-PAYMENT",
fallback = PaymentFallbackService.class // 这里
)
public interface PaymentHystrixService {

@GetMapping("/payment/hystrix/ok/{id}")
String paymentInfoOK(@PathVariable("id") Integer id);

@GetMapping("/payment/hystrix/timeout/{id}")
String paymentInfoTimeOut(@PathVariable("id") Integer id);
}

编写降级服务,注意这个降级服务必须继承自前面编写的 Feign 服务接口

@Component
public class PaymentFallbackService implements PaymentHystrixService {
@Override
public String paymentInfoOK(Integer id) {
return "服务端的 paymentInfoOK 方法请求异常";
}

@Override
public String paymentInfoTimeOut(Integer id) {
return "服务端的 paymentInfoTimeOut 方法请求异常";
}
}

模拟请求失败,下面将 yml 文件这个拉取远程服务信息的配置改成 false(这样就无法通过服务名来找到对应服务了)

eureka:
client:
fetch-registry: false # 改成 false 就无法拉取远程的服务信息了(无法通过服务名来找到对应服务)
service-url:
defaultZone: http://localhost:7001/eureka #单机版

最后在配置文件上启动这个 FallBack 支持

# 让 FallBack 生效
feign:
hystrix:
enabled: true

降级配置 @HystrixCommand

主要就是使用 @HystrixCommand 注解的 commandProperties 属性对请求进行配置(这两个注解看 SpringCloud Hystrix 基本使用 那篇笔记)

这个配置 @HystrixProperty 配置详情参考 官方文档 Configuration

如下配置了一个当超时 1500 秒就服务降级的 @HystrixCommand

@GetMapping("/consumer/payment/hystrix/timeout/{id}")
@HystrixCommand(
fallbackMethod = "paymentTimeOutFallBackMethod",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")
})
// @HystrixCommand
public String paymentInfoTimeOut(@PathVariable("id") Integer id) {
return "当前消费者调用生产者结果:" + paymentHystrixService.paymentInfoTimeOut(id);
}


public String paymentTimeOutFallBackMethod(@PathVariable("id") Integer id) {
return "当前请求的参数是:" + id + " 调用的 paymentTimeOutFallBackMethod 处于服务降级";
}

记得要把上面的 feign 的服务降级关掉,否则优先是走 feign 的

feign:
hystrix:
enabled: false

配置一个默认的全局的服务降级,注意配置了 fallbackMethod 的都会走它自己的降级方法,没有配置且加了 @HystrixCommand 注解的方法才会走全局的降级方法

如下所示

@RestController
@Slf4j
@DefaultProperties(defaultFallback = "paymentGlobalFallbackMethod")
public class OrderHystrixController {

/* ...*/

// 这里没有显示的指定 fallbackMethod,所以它会走全局的
@HystrixCommand
public String paymentInfoTimeOut(@PathVariable("id") Integer id) {
return "当前消费者调用生产者结果:" + paymentHystrixService.paymentInfoTimeOut(id);
}

public String paymentGlobalFallbackMethod() {
return "这是全局 fallback 方法";
}
}

添加请求缓存

image.png

1、请求缓存的生命周期是 一次请求内 2、原理就是把方法的参数当成一个 Key,方法的返回值当成一个 Value 3、在一次请求中,目标方法被调用过一次后结果就会被缓存

主要用到的注解

@CacheResult // 缓存当前方法的返回结果(配合 @HystrixCommand 使用)
@CacheRemove // 清除某个缓存信息(需要标识 commandKey)
@CacheKey // 指定方法的哪些参数作为缓存的 Key

创建一个 Service 用来专门处理这个缓存信息

@Service
public class CustomerService {
@Autowired
private SearchClient searchClient;

@CacheResult
@HystrixCommand(commandKey = "find") // 必须加上,用于与下面清除方法绑定 加上这个 @CacheKey 可以指定哪几个参数为 Key(默认是全部参数)
public Customer findById(@CacheKey Integer id) {
return searchClient.findById(id);
}

// 可以创建一个清除缓存的方法,key 要和上面的一样,且参数也要一致
@CacheRemove(commandKey = "find")
@HystrixCommand // 必须加上,但是可以啥也不写
public void clearFindById(@CacheKey Integer id) {
System.out.println("缓存被清空");
}
}

创建一个过滤器,使每次请求过来都会初始化这个 HystrixRequestContext

@WebFilter("/*")
public class HystrixRequestContextFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HystrixRequestContext context = HystrixRequestContext.initializeContext();
try {
// 该方法的作用是让 Filter 链上的当前过滤器放行,使请求进入下一个 Filter
// 如果不放行会一直卡在这里
filterChain.doFilter(servletRequest, servletResponse);
} finally {
// 释放资源
context.shutdown();
}
}
}

注意,同上,这个 @WebFilter 注解不归 Spring管,所以需要使用 @ServletComponentScan 注解去扫描这个包

@ServletComponentScan("com.alsritter.filter")

给远程服务返回值加个随机数,使之能更好的观察到缓存是否生效

// 远程的服务
@GetMapping("/search/{id}")
public Customer findById(@PathVariable Integer id) {
return new Customer(id, "张三", (int) (Math.random() * 1000));
}

Controller 就无需直接调用 searchClient 来获得远程方法的参数,而是先靠这个 Service 来取得

@Autowired
private CustomerService customerService;

@GetMapping("/search/{id}")
public Customer findById(@PathVariable Integer id) {
Customer byId = customerService.findById(id);
// 执行多次才能看出效果
System.out.println(customerService.findById(id));
System.out.println(customerService.findById(id));
// 这里清空后再调用,看是否值改变,如果改变表示缓存已经被清空了
customerService.clearFindById(id);
// 这里调用两次结果是一样的(会重新缓存)
System.out.println(customerService.findById(id));
System.out.println(customerService.findById(id));

return byId;
}